﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using HtmlAgilityPack;
using System.Text.RegularExpressions;

namespace Util.Html
{
    public class HtmlUtility
    {
        private static Regex trimRightMatch; // 字符串尾部的空格和换行符匹配
        private static Regex trimLeftMatch; // 字符串首部的空格、换行符、点、顿号、逗号匹配

        static HtmlUtility()
        {
            trimRightMatch = new Regex(@"((<br>)|(&nbsp;)|[\s])*$", RegexOptions.Compiled);
            trimLeftMatch = new Regex(@"^((<br>)|(&nbsp;)|[\s\.．、，,])*", RegexOptions.Compiled);
        }

        /*
         * 把Html文件加载一颗Html树
         * @param htmlFile(FileInfo){not null}
         * Html文件
         * @return HtmlNode{can null}
         * Html根节点,如果Html文件不存在或者加载失败,则返回null
         * @throw ArgumentNullException
         * Html文件对象为空
         */
        public static HtmlNode LoadHtml(FileInfo htmlFile)
        {
            if (htmlFile == null)
            {// 如果字符串对象为空,则抛出空对象异常
                throw new ArgumentNullException("htmlFile");
            }
            if (!htmlFile.Exists)
            {
                return null;
            }
            HtmlDocument doc = new HtmlDocument(); // 创建Html文档
            try
            {
                doc.Load(htmlFile.FullName); // 加载Html文件
                return doc.DocumentNode; // 返回Html根节点
            }
            catch (Exception) { return null; }
        }
        /*
         * 把Html字符串加载一颗Html树
         * @param htmlFile(string){not null}
         * Html字符串
         * @return HtmlNode{can null}
         * Html根节点,如果该字符串为空串或加载失败,则返回null
         * @throw ArgumentNullException
         * Html字符串对象为空
         */
        public static HtmlNode LoadHtml(string htmlString)
        {
            if (htmlString == null)
            {// 如果字符串对象为空,则抛出空对象异常
                throw new ArgumentNullException("htmlString");
            }
            if (string.IsNullOrWhiteSpace(htmlString))
            {
                return null;
            }
            HtmlDocument doc = new HtmlDocument(); // 创建Html文档
            try
            {
                doc.LoadHtml(htmlString); // 加载Html字符串
                return doc.DocumentNode; // 返回Html根节点
            }
            catch (Exception)
            {
                return null;
            }
        }

        /*
         * 用当前节点的所有孩子节点替换到当前节点的位置上
         * @param current(HtmlNode){not null}
         * 要替换的Html节点
         * @return bool
         * 当前节点没有父节点则返回false
         * @throw ArgumentNullException
         * 当前节点对象为空
         */
        public static bool ReplaceNodeByChildren(HtmlNode current)
        {
            if (current == null)
            {// 当前节点为空对象,则抛出空对象异常
                throw new ArgumentNullException("current");
            }
            HtmlNode parent = current.ParentNode;
            if (parent == null)
            { // 当前节点没有父节点,直接返回false
                return false;
            }
            HtmlNodeCollection children = current.ChildNodes; // 获取当前节点的所有孩子节点
            HtmlNode next = current.NextSibling; // 获取当前节点的右兄弟节点
            if (next == null)
            { // 如果当前节点是父节点的最后一个孩子节点,则直接把当前节点的所有孩子节点添加到父节点上
                parent.AppendChildren(children);
            }
            else
            {
                foreach (HtmlNode child in children)
                { // 把当前节点的所有孩子节点添加到有兄弟节点的左侧.
                    parent.InsertBefore(child, next);
                }
            }
            current.Remove(); // 移除当前节点
            return true;
        }
        /*
         * 移除当前节点的所有属性
         * @param current(HtmlNode){not null}
         * 要移除属性的Html节点
         * @throw ArgumentNullException
         * 当前节点对象为空
         */
        public static void RemoveAllAttributes(HtmlNode current)
        {
            if (current == null)
            {// 当前节点为空对象,则抛出空对象异常
                throw new ArgumentNullException("current");
            }
            current.Attributes.RemoveAll();// 移除节点的所有属性
        }

        /*
         * 移除当前节点的一些属性
         * @param current(HtmlNode){not null}
         * 要移除属性的Html节点
         * @param excepts(string[]){can null}
         * 要保留的属性,这个列表中的属性名将不被移除
         * @throw ArgumentNullException
         * 当前节点对象为空
         */
        public static void RemoveAttributes(HtmlNode current, params string[] excepts)
        {
            RemoveAttributes(current, excepts.AsEnumerable());
        }

        /*
         * 移除当前节点的一些属性
         * @param current(HtmlNode){not null}
         * 要移除属性的Html节点
         * @param excepts(IEnumerable<string>){can null}
         * 要保留的属性,这个列表中的属性名将不被移除
         * @throw ArgumentNullException
         * 当前节点对象为空
         */
        public static void RemoveAttributes(HtmlNode current, IEnumerable<string> excepts)
        {
            if (current == null)
            {// 当前节点为空对象,则抛出空对象异常
                throw new ArgumentNullException("current");
            }
            if (!current.HasAttributes)
            {// 如果当前节点没有属性,直接返回
                return;
            }
            if (excepts == null || excepts.Count() == 0)
            {// 如果没有保留的属性,则直接移除所有属性
                current.Attributes.RemoveAll();
                return;
            }
            List<HtmlAttribute> removes = current.Attributes.Where(t => !excepts.Contains(t.Name)).ToList(); // 获取要移除的属性
            removes.ForEach(t => current.Attributes.Remove(t)); // 移除这些属性
        }
        /*
         * 用另一个Html节点替换当前节点
         * @param current(HtmlNode){not null}
         * 要被替换的Html节点
         * @param replaced(HtmlNode){not null}
         * 用来替换的新Html节点
         * @return bool
         * 如果当前节点没有父节点,则返回false
         * @throw ArgumentNullException
         * 当前节点或替换节点的对象为空
         */
        public static bool ReplaceNode(HtmlNode current, HtmlNode replaced)
        {
            if (current == null)
            { // 当前节点为空,抛出空对象异常
                throw new ArgumentNullException("current");
            }
            if (replaced == null)
            {// 用来替换的新节点为空,抛出空对象一场
                throw new ArgumentNullException("replaced");
            }
            HtmlNode parent = current.ParentNode;
            if (parent == null)
            { // 当前节点没有父节点,直接返回false
                return false;
            }
            parent.ReplaceChild(replaced, current); // 替换节点
            return true;
        }

        /*
         * 获取当前Html节点的style属性
         * @param current(HtmlNode){not null}
         * 要获取style属性的Html节点
         * @return Dictionary<string, string>{not null}
         * 键值对集合的style属性
         * @throw ArgumentNullException
         * 当前节点对象为空
         * @throw InvalidDataException
         * 当前节点的style属性有错误的值
         */
        public static Dictionary<string, string> GetStyleAttributes(HtmlNode current)
        {
            if (current == null)
            {// 当前节点为空,抛出空对象异常
                throw new ArgumentNullException("current");
            }
            Dictionary<string, string> values = new Dictionary<string, string>();
            if (!current.Attributes.Contains("style"))
            { // 如果当前节点没有style属性,直接返回空键值对集合
                return values;
            }
            string styleString = current.Attributes["style"].Value;
            if (string.IsNullOrWhiteSpace(styleString))
            {// 如果当前节点的style属性为空串,直接返回空键值对集合
                return values;
            }
            string[] styles = styleString.Split(';'); // 用分号切分style的值
            foreach (string style in styles)
            {
                if (string.IsNullOrWhiteSpace(style))
                { // 过滤空串
                    continue;
                }
                string[] options = style.Split(':'); // 用冒号切分键值对
                switch (options.Count())
                { // 数组一般是一个或两个
                    case 1:
                        values.Add(options[0].Trim(), null);
                        break;
                    case 2:
                        values.Add(options[0].Trim(), options[1].Trim());
                        break;
                    default:
                        throw new InvalidDataException("style: " + style + " 错误"); // style属性有错误的值
                }
            }
            return values;
        }
        /*
         * 设置当前Html节点的style属性
         * @param current(HtmlNode){not null}
         * 要设置style属性的Html节点
         * @param values(Dictionary<string, string>){can null}
         * 要设置的键值对集合
         * @throw ArgumentNullException
         * 当前节点对象为空
         */
        public static void SetStyleAttributes(HtmlNode current, Dictionary<string, string> values)
        {
            if (current == null)
            {// 当前节点为空,抛出空对象异常
                throw new ArgumentNullException("current");
            }
            if (values == null || values.Count == 0)
            {// 如果键值对集合为空,直接返回
                return;
            }
            StringBuilder styleString = new StringBuilder();
            foreach (KeyValuePair<string, string> pair in values)
            {// 遍历键值对集合,把style属性取出来
                styleString.Append(pair.Key);
                if (!string.IsNullOrWhiteSpace(pair.Value))
                {
                    styleString.Append(":" + pair.Value);
                }
                styleString.Append(";");
            }
            styleString.Remove(styleString.Length - 1, 1); // 移除最后的一个分号
            current.Attributes["style"].Value = styleString.ToString(); // 设置style值
        }

        /*
         * 矫正当前Html节点的属性到style属性
         * @param current(HtmlNode){not null}
         * 要矫正的Html节点
         * @throw ArgumentNullException
         * 当前节点对象为空
         */
        public static void FixStyleAttributes(HtmlNode current)
        {
            if (current == null)
            {// 当前节点为空,抛出空对象异常
                throw new ArgumentNullException("current");
            }
            Dictionary<string, string> values = GetStyleAttributes(current); // 获取当前节点的style属性
            HandleAlignAttribute(current, values); // 矫正align属性
            HandleSpaceAttribute(current, values); // 矫正space属性
            SetStyleAttributes(current, values); // 重新设置style属性
        }

        /*
         * 删除html字符串开头和结尾没用的字符串信息
         * @param html(string){not null}
         * 要处理的字符串
         * @throw ArgumentNullException
         * 字符串对象为空
         */
        public static string TrimHtmlString(string html)
        {
            if (html == null)
            {// 字符串对象为空对象,抛出空对象异常
                throw new ArgumentNullException("html");
            }
            html = trimLeftMatch.Replace(trimRightMatch.Replace(html.Trim(), ""), "");
            return html;
        }

        /*
         * 判断当前Html节点是不是没有孩子的节点,如果是则移除该节点
         * @param current(HtmlNode){not null}
         * 要操作的Html节点
         * @param recursion(bool)
         * 是否递归到父节点
         * @return bool
         * 如果该Html节点有孩子节点,则返回false
         * @throw ArgumentNullException
         * 当前节点为空
         */
        public static bool RemoveEmptyNode(HtmlNode current, bool recursion)
        {
            if (current == null)
            { // 当前节点为空,抛出空对象异常
                throw new ArgumentNullException("current");
            }
            bool remove = true;
            foreach (HtmlNode child in current.ChildNodes)
            {
                if (child.NodeType == HtmlNodeType.Element)
                {//如果有一个孩子节点是元素节点,则当前节点不为空
                    remove = false;
                    break;
                }
            }
            if (remove)
            { // 移除当前节点
                HtmlNode parent = current.ParentNode;
                if (parent != null && recursion)
                { // 是否递归父节点
                    RemoveEmptyNode(parent, recursion);
                }
                current.Remove();
            }
            return remove;
        }

        private static void HandleSpaceAttribute(HtmlNode current, Dictionary<string, string> values)
        {
            //
        }

        /*
         * 
         */
        public static bool IsOnlyChildNode(HtmlNode current, params string[] names)
        {
            if (current == null)
            {
                throw new Exception("静态函数:IsOnlyChildNode 参数:current 空指针");
            }
            if (names == null)
            {
                throw new Exception("静态函数:IsOnlyChildrenNodes 参数:names 空指针");
            }
            bool only = false;
            foreach (HtmlNode child in current.ChildNodes)
            {
                if (child.NodeType != HtmlNodeType.Element)
                {
                    return false;
                }
                string name = child.Name;
                if (name.Equals("br"))
                { // 过滤br标签
                    continue;
                }
                if (only || !names.Contains(name))
                {
                    return false;
                }
                only = true;
            }
            return only;
        }

        /*
         * 判断当前Html节点的孩子节点是不是只有一个img或table标签,br标签可随意
         */
        public static bool IsOnlyGraphChildNode(HtmlNode current)
        {
            return IsOnlyChildNode(current, "img", "table", "br");
        }

        public static bool IsOnlyChildrenNodes(HtmlNode current, string name)
        {
            return IsOnlyChildrenNodes(current, new string[] { name });
        }

        public static bool IsOnlyChildrenNodes(HtmlNode current, string[] names)
        {
            if (current == null)
            {
                throw new Exception("静态函数:IsOnlyChildrenNodes 参数:current 空指针");
            }
            if (names == null)
            {
                throw new Exception("静态函数:IsOnlyChildrenNodes 参数:names 空指针");
            }
            if (!current.HasChildNodes)
            {
                return false;
            }
            foreach (HtmlNode child in current.ChildNodes)
            {
                if (child.NodeType != HtmlNodeType.Element)
                {
                    return false;
                }
                string name = child.Name;
                if (!name.Equals("br") && !names.Contains(name))
                {
                    return false;
                }
            }
            return true;
        }

        public static bool IsOnlyTextValueNode(HtmlNode current)
        {
            if (current == null)
            {
                throw new Exception("静态函数:IsOnlyTextValueNode 参数:current 空指针");
            }
            if (!current.HasChildNodes)
            {
                return false;
            }
            HtmlNodeCollection children = current.ChildNodes;
            foreach (HtmlNode child in children)
            {
                if (child.NodeType != HtmlNodeType.Text)
                {
                    string name = child.Name;
                    if (!name.Equals("sup") && !name.Equals("img") && !name.Equals("sub") && !name.Equals("br") && !name.Equals("frm"))
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        /*
         * 判断当前Html节点有没有该属性并属性的值有没有该字符串
         * @param current(HtmlNode){not null}
         * 要判断的Html节点
         * @param name(string){not null}
         * 要判断的属性名称
         * @param value(string){can null}[@default=null]
         * 要判断的属性值
         * @return bool
         * 当前节点有该属性并且属性值有value字符串则返回true
         * @throw 
         */
        public static bool IsContainsAttribute(HtmlNode current, string name, string value = null)
        {
            if (current == null)
            {
                throw new Exception("静态函数:IsContainsAttribute 参数:current 空指针");
            }
            if (string.IsNullOrWhiteSpace(name))
            {
                throw new Exception("静态函数:IsContainsAttribute 参数:name 空指针或空串");
            }
            if (!current.HasAttributes || !current.Attributes.Contains(name))
            {
                return false;
            }
            if (value == null)
            {
                return true;
            }
            return current.Attributes[name].Value.Contains(value);
        }
        /*
         * 
         */
        public static bool IsEqualsAttribute(HtmlNode current, string name, string value = null)
        {
            if (current == null)
            {
                throw new ArgumentNullException("current");
            }
            if (string.IsNullOrWhiteSpace(name))
            {
                throw new ArgumentNullException("name");
            }
            if (!current.HasAttributes || !current.Attributes.Contains(name))
            {
                return false;
            }
            if (value == null)
            {
                return true;
            }
            return current.Attributes[name].Value.Equals(value);
        }

        /*
         *  判断当前Html节点是不是元素节点,并且该节点的名称是不是name
         *  @param current(HtmlNode){not null}
         *  要判断的Html节点
         *  @param name(string){not null}
         *  要判断的节点名称
         *  @return bool
         *  该节点是元素节点并且名称对应则返回true
         */
        public static bool IsName(HtmlNode current, string name)
        {
            if (current == null)
            {
                throw new ArgumentNullException("current");
            }
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            if (current.NodeType != HtmlNodeType.Element)
            {
                return false;
            }
            return current.Name.Equals(name);
        }

        static private void HandleAlignAttribute(HtmlNode current, Dictionary<string, string> values)
        {
            //if (current.Attributes.Contains("align"))
            //{
            //    string value = current.Attributes["align"].Value;
            //    if (value.Equals("left"))
            //    {
            //        values.Add("float", "left");
            //    }
            //    else if (value.Equals("right"))
            //    {
            //        values.Add("float", "right");
            //    }
            //    else if (value.Equals("center") || value.Equals("absmiddle") || value.Equals("middle"))
            //    {
            //        values.Add("margin", "0 auto");
            //    }
            //    current.Attributes.Remove("align");
            //}
        }


        /*
         *  遍历当前Html节点,移除空的文本节点和注释,并返回移除后的innerHtml
         *  @param parent(HtmlNode){not null}
         *  要处理的Html节点
         *  @return string
         *  移除后的节点的innerHtml
         *  @throw ArgumentNullException
         *  当前节点为空
         */
        public static string TidyInnerHtml(HtmlNode parent)
        {
            if (parent == null)
            {
                throw new ArgumentNullException("parent");
            }
            HtmlNodeCollection children = parent.SelectNodes(".//text() | .//comment()"); // 获取所有文本和注释的孩子节点
            if (children == null)
            { // 如果当前节点没有文本和注释的孩子节点,直接返回innerHtml
                return parent.InnerHtml;
            }
            foreach (HtmlNode child in children)
            {
                if (child.NodeType == HtmlNodeType.Text)
                { // 如果该孩子节点是文本节点,则对文本进行处理
                    string text = child.InnerText;
                    text = text.Trim().Replace("\n", "").Replace("\t", ""); // 清除\r\t的文本格式符号
                    text = TrimHtmlString(text); // 清除首部和尾部的没用符号
                    if (string.IsNullOrWhiteSpace(text))
                    { // 如果处理后的节点是空节点,则移除该节点
                        child.Remove();
                        continue;
                    }
                    child.InnerHtml = text;
                }
                else if (child.NodeType == HtmlNodeType.Comment)
                {  // 如果该孩子是注释节点,直接移除
                    child.Remove();
                }
            }
            return parent.InnerHtml;
        }
    }
}
